﻿using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections;

namespace Utils.Wpf.Chart
{
    public partial class Chart : INotifyPropertyChanged
    {
        #region Attributes

        private DateTime? _minX;
        private DateTime? _maxX;

        private readonly ChartAxisCollection _axis;

		private readonly Line _crossX;
        private readonly Line _crossY;

        #endregion

        #region Dependence Properties

        private static readonly DependencyPropertyKey SeriesPropertyKey = DependencyProperty.RegisterReadOnly("Series", typeof(ChartSerieCollection), typeof(Chart), new FrameworkPropertyMetadata(new ChartSerieCollection()));
        public static DependencyProperty SeriesProperty = SeriesPropertyKey.DependencyProperty;
        public static DependencyProperty CrossBrushProperty = DependencyProperty.Register("CrossBrush", typeof(Brush), typeof(Chart), new FrameworkPropertyMetadata(OnCrossBrushPropertyChanged));
        public static DependencyProperty AxisBrushProperty = DependencyProperty.Register("AxisBrush", typeof(Brush), typeof(Chart), new FrameworkPropertyMetadata(OnAxisBrushPropertyChanged));

        #endregion

        #region Properties

        public ChartSerieCollection Series
        {
            get
            {
                return (ChartSerieCollection)base.GetValue(SeriesProperty);
            }
            set
            {
                base.SetValue(SeriesProperty, value);
            }
        }

        public Brush CrossBrush
        {
            get
            {
                return (Brush)base.GetValue(CrossBrushProperty);
            }
            set
            {
                base.SetValue(CrossBrushProperty, value);
            }
        }

		public Brush AxisBrush
		{
			get
			{
				return (Brush)base.GetValue(AxisBrushProperty);
			}
			set
			{
				base.SetValue(AxisBrushProperty, value);
			}
		}

        public string MaxYCaption
        {
            get
            {
            	return _axis.MaxYCaption;
            }
        }

        public string MinYCaption
        {
			get
			{
				return _axis.MinYCaption;
			}
        }

        public DateTime? MaxX
        {
            get
            {
                return _maxX;
            }
            set
            {
                if (_maxX != value)
                {
                    _maxX = value;
                    this.OnPropertyChanged("MaxX");
                }
            }
        }

        public DateTime? MinX
        {
            get
            {
                return _minX;
            }
            set
            {
                if (_minX != value)
                {
                    _minX = value;
                    this.OnPropertyChanged("MinX");
                }
            }
        }

        protected override IEnumerator LogicalChildren
        {
            get
            {
                if (this.Series == null)
                {
                    yield break;
                }

                foreach (var artist in this.Series)
                {
                    yield return artist;
                }
            }
        }

        #endregion

        #region Public Methods

        public Chart()
        {
        	_axis = new ChartAxisCollection();
			_axis.PropertyChanged += Axis_PropertyChanged;

        	ChartSerieCollection series = new ChartSerieCollection();
            this.SetValue(Chart.SeriesPropertyKey, series);
            series.CollectionChanged += Series_CollectionChanged;
			
            _crossX = new Line();
            _crossY = new Line();
            
            _crossX.StrokeThickness = 1;
            _crossY.StrokeThickness = 1;

            _crossX.Stroke = Brushes.White;
            _crossX.Stroke = Brushes.White;

            InitializeComponent();

            this.DrawArea.Children.Add(_crossX);
            this.DrawArea.Children.Add(_crossY);
        }

        #endregion

        #region Event Handlers

		private void Axis_PropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			this.OnPropertyChanged(e.PropertyName);
		}

        private void Series_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                ChartSerie newitem = (ChartSerie)e.NewItems[0];
                newitem.PropertyChanged += Serie_PropertyChanged;
                newitem.Line.StrokeThickness = newitem.Thickness;
            	newitem.Line.Stroke = newitem.Brush;

                this.AddLogicalChild(newitem);
            }
        }

        private void Serie_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            ChartSerie serie = (ChartSerie)sender;

            if (e.PropertyName == "Data")
            {
                serie.Data.CollectionChanged += this.Data_CollectionChanged;
                this.CalculateMaxMin(serie);
                this.DrawSerie(serie);
            }
            else if (e.PropertyName == "Brush")
            {
                serie.Line.Stroke = serie.Brush;
            }
			else if (e.PropertyName == "Thickness")
			{
                serie.Line.StrokeThickness = serie.Thickness;
			}
			else if (e.PropertyName == "Visibility") // If the serie is not visible its points can not be created because the max/min is not calculated for them.
            {
                this.ResetMaxMin();
                this.DrawAllSeries();
            }
			else if (e.PropertyName == "AxisIndex")
			{
				while (serie.AxisIndex + 1 > _axis.Items.Count)
				{
					_axis.Items.Add(new ChartAxis());
				}
			}
        }

        private void Data_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (this.Dispatcher.CheckAccess())
            {
            	ChartSerie serie = this.FindSerieFromData((ObservableCollection<ChartPoint>) sender);

				if (serie == null)
					throw new InvalidOperationException("The serie has not been found");

                if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
                {
                    foreach (ChartPoint point in e.NewItems)
                    {
                        this.AddPointToSerie(point, serie);
                    }
                }
                else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
                {
					foreach (Rectangle drawPoint in serie.LinePoints)
					{
						this.DrawArea.Children.Remove(drawPoint);
					}
					serie.Clear();
                }
            }
            else
            {
				this.Dispatcher.BeginInvoke(new Action<object, System.Collections.Specialized.NotifyCollectionChangedEventArgs>(this.Data_CollectionChanged), new object[] { sender, e });	
            }
        }

        private static void OnCrossBrushPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            Chart control = (Chart)source;
            control._crossX.Stroke = (Brush)e.NewValue;
            control._crossY.Stroke = (Brush)e.NewValue;
        }

		private static void OnAxisBrushPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            Chart control = (Chart)source;
            control.Border1.Stroke = (Brush)e.NewValue;
            control.Border2.Stroke = (Brush)e.NewValue;
        }

		private void DrawArea_MouseEnter(object sender, MouseEventArgs e)
		{
			_crossX.Visibility = System.Windows.Visibility.Visible;
			_crossY.Visibility = System.Windows.Visibility.Visible;
			this.CrossValue.Visibility = System.Windows.Visibility.Visible;
		}

		private void DrawArea_MouseLeave(object sender, MouseEventArgs e)
		{
			_crossX.Visibility = System.Windows.Visibility.Collapsed;
			_crossY.Visibility = System.Windows.Visibility.Collapsed;
			this.CrossValue.Visibility = System.Windows.Visibility.Collapsed;
		}

		private void DrawArea_MouseMove(object sender, MouseEventArgs e)
		{
			const string caption = "[{0},{1}]";
			string x = "-";
			string y = "-";

			Point mousePosition = System.Windows.Input.Mouse.GetPosition(this.DrawArea);

			_crossX.X1 = 0;
			_crossX.X2 = this.DrawArea.ActualWidth;
			_crossX.Y1 = _crossX.Y2 = mousePosition.Y;

			_crossY.Y1 = 0;
			_crossY.Y2 = this.DrawArea.ActualHeight;
			_crossY.X1 = _crossY.X2 = mousePosition.X;

			// X Caption
			if (_maxX != null && _minX != null)
			{
				DateTime xValue = _minX.Value + new TimeSpan(0,0,(int)((_maxX.Value - _minX.Value).TotalSeconds / this.DrawArea.ActualWidth * mousePosition.X));
				x = xValue.ToShortTimeString();
			}

			for (int index = 0; index < _axis.Items.Count; index++)
			{
				if (_axis.Items[index].MaxY == null || _axis.Items[index].MinY == null)
				{
					y += Environment.NewLine;
				}
				else
				{
					double yValue = (double)(_axis.Items[index].MaxY.Value - _axis.Items[index].MinY.Value) / this.DrawArea.ActualHeight * (this.DrawArea.ActualHeight - mousePosition.Y) + (double)_axis.Items[index].MinY.Value;
					y += FormatNumberWithChartPrecission(yValue);
					if (index != _axis.Items.Count - 1)
						y += "\n";
				}				
			}

			this.CrossValue.Text = string.Format(caption,x,y);

			// TODO: Measure the size of the string
			if (mousePosition.X > this.DrawArea.ActualWidth /2)
			{
				Canvas.SetLeft(this.CrossValue, mousePosition.X - 80);	
			}
			else
			{
				Canvas.SetLeft(this.CrossValue, mousePosition.X + 2);
			}

			if (mousePosition.Y > this.DrawArea.ActualHeight/ 2)
			{
				Canvas.SetTop(this.CrossValue, mousePosition.Y - 17);
			}
			else
			{
				Canvas.SetTop(this.CrossValue, mousePosition.Y + 2);
			}
		}

        private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            this.DrawAllSeries();
        }

        #endregion

        #region Private Methods

        private void ResetMaxMin()
        {
            _minX = null;
            _maxX = null;
        	foreach (ChartAxis axis in _axis.Items)
        	{
				axis.MaxY = null;
				axis.MinY = null;
        	}
        }

        private void DrawAllSeries()
        {
            foreach (ChartSerie serie in this.Series)
            {
                if (serie.Data != null && serie.Visibility == Visibility.Visible)
                    this.CalculateMaxMin(serie);
            }

            foreach (ChartSerie serie in this.Series)
            {
                if (serie.Data != null)
                    this.DrawSerie(serie);
            }
        }

        private void DrawSerie(ChartSerie serie)
        {
            // Remove old data
            this.DrawArea.Children.Remove(serie.Line);
            foreach (Rectangle drawPoint in serie.LinePoints)
            {
                this.DrawArea.Children.Remove(drawPoint);
            }
			serie.Clear();

            if (serie.Visibility == System.Windows.Visibility.Visible)
            {
                this.DrawArea.Children.Add(serie.Line);
            	ObservableCollection<ChartPoint> points = serie.Data.GetReadLock();
				foreach (ChartPoint point in points)
                {
					this.AddPointToSerieFinal(point, serie);
                }
				serie.Data.FreeReadLock();
            }
        }

    	private void AddPointToSerie(ChartPoint point, ChartSerie serie)
        {
            if (serie.Visibility == System.Windows.Visibility.Visible)
            {
                if (this.CalculateMaxMin(point, serie))
                {
                    this.DrawAllSeries();
                }
                else
                {
                	this.AddPointToSerieFinal(point, serie);
                }
            }
        }

		private void AddPointToSerieFinal(ChartPoint point, ChartSerie serie)
		{
			Point screenPoint = this.CalculateScreenPoint(point, serie);
			serie.Line.Points.Add(screenPoint);

			if (serie.DrawPoints)
			{
				Rectangle drawPoint = new Rectangle();
				drawPoint.Fill = serie.Brush;
				drawPoint.Height = 4;
				drawPoint.Width = 4;
				this.DrawArea.Children.Add(drawPoint);
				Canvas.SetTop(drawPoint, screenPoint.Y - 2);
				Canvas.SetLeft(drawPoint, screenPoint.X - 2);
				serie.LinePoints.Add(drawPoint);
			}
		}

    	private Point CalculateScreenPoint(ChartPoint point, ChartSerie serie)
        {
        	ChartAxis axis = _axis.Items[serie.AxisIndex];

			if (_minX == null || _maxX == null || axis.MaxY == null || axis.MinY == null)
				throw new InvalidOperationException("The minimum or maximum values have not been set");

			double x = (point.X - _minX.Value).TotalSeconds * this.DrawArea.ActualWidth / (_maxX.Value - _minX.Value).TotalSeconds;
			double y = ((double)(axis.MaxY.Value - point.Y) * this.DrawArea.ActualHeight) / (double)(axis.MaxY.Value - axis.MinY.Value);

        	return new Point((int) x, (int) y);
        }

        private bool CalculateMaxMin(ChartSerie serie)
        {
            bool valuesChanged = false;

			ObservableCollection<ChartPoint> points = serie.Data.GetReadLock();
			foreach (ChartPoint point in points)
            {
				valuesChanged |= this.CalculateMaxMin(point, serie);
            }
			serie.Data.FreeReadLock();

            return valuesChanged;
        }

		private bool CalculateMaxMin(ChartPoint point, ChartSerie serie)
        {
			ChartAxis axis = _axis.Items[serie.AxisIndex];

			bool valuesChanged = this.CalculateMaxMinX(point);
			valuesChanged |= this.CalculateMaxMinY(axis, point);

            return valuesChanged;
        }

		protected virtual bool CalculateMaxMinX(ChartPoint point)
		{
			bool valuesChanged = false;

			if (_maxX == null || point.X > _maxX)
			{
				this.MaxX = point.X + new TimeSpan(0, 0, (int)(point.X.TimeOfDay.TotalSeconds * 0.01));
				valuesChanged = true;
			}
			if (_minX == null || point.X < _minX)
			{
				this.MinX = point.X - new TimeSpan(0, 0, (int)(point.X.TimeOfDay.TotalSeconds * 0.01));
				valuesChanged = true;
			}

			return valuesChanged;
		}

		private bool CalculateMaxMinY(ChartAxis axis, ChartPoint point)
		{
			bool valuesChanged = false;

			if (axis.MaxY == null || point.Y > axis.MaxY)
			{
				axis.MaxY = point.Y;
				valuesChanged = true;
			}
			if (axis.MinY == null || point.Y < axis.MinY)
			{
				axis.MinY = point.Y;
				valuesChanged = true;
			}

			return valuesChanged;
		}

		private static string FormatNumberWithChartPrecission(double number)
		{
			if (number > 100)
			{
				return number.ToString("#");
			}
			else if (number > 10)
			{
				return number.ToString("#.#");
			}
			else
			{
				return number.ToString("#.##");
			}
		}

		private ChartSerie FindSerieFromData(ObservableCollection<ChartPoint> data)
		{
			ChartSerie serie = null;
			foreach (ChartSerie chartSerie in this.Series)
			{
				if (chartSerie.Data != null)
				{
					try
					{
						if (chartSerie.Data.GetReadLock() == data)
						{
							serie = chartSerie;
							break;
						}
					}
					finally
					{
						chartSerie.Data.FreeReadLock();
					}
				}
			}

			return serie;
		}

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion


    }
}
